package auction.client;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Random;

import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.log4j.Logger;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;

import auction.securechannel.AESChannel;
import auction.securechannel.Channel;
import auction.securechannel.RSAChannel;
import auction.securechannel.TCPChannel;
import auction.securechannel.Base64Channel;

import auction.serveroutage.ClientReconnectThread;
import auction.serveroutage.ClientTimeStampServer;


public class Client {
	public static Logger log = Logger.getLogger(Client.class);

	private static ReceiverThread serverReceiver;
	private static Socket serverSocket;
	private static BufferedReader inReadConsole;
	private static PrintWriter serverOut;


	//list of auctions received from server after !list
	private static String[] auctionList = new String[0];

	private static String serverHost;
	private static int serverPort;
	private static int clientPort;
	private static String serverKeyPub;
	private static String clientsKeyDir;

	private static Key key=null;
	private static String username;


	private static Channel decoratedChannel;
	private static String clientChallenge;
	private static String serverChallenge;
	private static SecretKey secretKey;
	private static IvParameterSpec ivParameter;

	//list of clients received from server on any login of a user - !getClientList
	private static String[] clientList = new String[0];
	private static Socket client1Socket;
	private static String client1Host;
	private static int client1Port;
	private static String client1Username;
	private static Socket client2Socket;
	private static String client2Host;
	private static int client2Port;
	private static String client2Username;

	//Keys for RSA
	private static PrivateKey privateKey;
	private static PublicKey publicKey;

	private static boolean ended = false;//client ended if server is unavailable
	private static boolean loggedIn = false;
	private static String input;
	private static ArrayList<String> signedBids;
	private static boolean refresh = false;

	public static void main(String[] args) {
		/* check arguments */

		boolean correctArgs = false;

		//Assignment 3:
		/*  <arg value="${server.host}"/>
			<arg value="${server.port}"/>
			<arg value="${client1.port}"/>
			<arg value="${server.key.pub}"/>
			<arg value="${clients.key.dir}"/>*/

		if( args.length == 5 && args[1].matches("[0-9]+")
				&& args[2].matches("[0-9]+") )
			correctArgs=true;

		if (correctArgs) {
			serverHost = args[0];
			serverPort = Integer.parseInt(args[1]);
			clientPort = Integer.parseInt(args[2]);
			setServerKeyPub(args[3]);
			clientsKeyDir = args[4];

			correctArgs = correctArgs && serverPort > 0 && serverPort < 65536;
			correctArgs = correctArgs && clientPort > 0
					&& clientPort < 65536;
			correctArgs = correctArgs && args[0].length() > 0;
		}
		//Close if arguments are invalid
		if (!correctArgs) {
			System.err
			.println("Usage: java auction.client.Client <server host> <server port> <client port> <server.key.pub> <clients.key.dir>");
			System.exit(1);
		}

		try {
			serverSocket = new Socket(serverHost, serverPort);

			//Instantiate channel before receiver, because in the receiver the channel frm the client is used
			decoratedChannel = new RSAChannel(new Base64Channel(new TCPChannel(serverSocket)));

			(serverReceiver = new ReceiverThread(serverSocket)).start();

			inReadConsole = new BufferedReader(new InputStreamReader(System.in));

			//Stage 4
			ClientTimeStampServer clientTimeStampServer = new ClientTimeStampServer(clientPort);
			clientTimeStampServer.start();

		} catch (IOException e) {
			System.err.println("Error while initialization: " + e.getMessage());
			cleanUp(1);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error while initialization: " + e.getMessage());
			cleanUp(1);
		} catch (NoSuchPaddingException e) {
			System.err.println("Error while initialization: " + e.getMessage());
			cleanUp(1);
		}

		//Read from console loop
		while (true) {
			input = "";
			try {

				input = inReadConsole.readLine();

			} catch (Exception e) {
				log.error("Error while processing input: "
						+ e.getMessage());
				cleanUp(1);
			}

			//Clientlist for Stage4
			if (input.startsWith("!getClientList") ) {
				if(Client.isLoggedIn())
					Client.printClientList();//print local ClientList
				else
					System.out.println("You have to log in first!");


			} 
			else{

				if(!serverSocket.isClosed()){

					//Block (send no) input if client is locked at confirming status
					if(!(serverReceiver.getBlockstatus())){
						if(input.startsWith("!login")){
							if(input.length()>7){
								//password = null;//reset password on new login
								username = input.substring(7);
								if(!readKeyfile(username)){
									continue;
								}
								if(!readRSAPrivateKey())
									continue;
								if(!readRSAPublicKey())
									continue;

								//generate ClientChallenge
								try {
									clientChallenge = generateClientChallenge();

								} catch (UnsupportedEncodingException e) {
									log.debug("Fehler beim Generieren der ClientChallenge");
									log.error("Anfrage kann nicht gesendet werden");
								}
								//send input+port+challenge through channel
								decoratedChannel.send((input+" "+clientPort+" "+clientChallenge).getBytes());
							}
							else 
								System.out.println("Usage: !login <username>");
						}
						else{
							//send input through channel
							decoratedChannel.send((input).getBytes());
						}
					}
				}
				else{
					log.debug("Socket is closed");
					//Accept only !end and !exit command and !bid command if user was logged in befor socket was closed

					if(input.equals("!end")||input.equals("!exit")){
						Client.cleanUp(0);
						Client.setEnded(true);
					}
					else if(input.startsWith("!bid")){
						if(Client.isLoggedIn()){
							log.debug("command !bid -> make signed bid");

							if(input.length()>5)
							{
								String[] s = input.substring(5).split(" ");

								/* check input */
								correctArgs = s.length == 2 && s[0].matches("[0-9]+") && s[1].matches("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?");
								String auctionId = s[0];
								String price = s[1];

								if (correctArgs) {
									//obtain the current timestamp from 2 random clients
									String signedBid = Client.obtainTimeStampsFromTwoClients(auctionId, price);

									if(!signedBid.equals("")){
										if(signedBids==null)
											signedBids = new ArrayList<String>();

										//add new signed bid to arraylist
										signedBids.add(signedBid);
										log.info("signedBid was generated, once the Server is available again the !bid command will be processed.");
									}
								}
								else 
									System.out.println("Usage: !bid <auction-id> <amount> - the id has to be a number, and the amount a float with a '.' as separator! ");							
							}
							else
								System.out.println("Usage: !bid <auction-id> <amount>");

						}
						else
							System.out.println("You have to log in first!");
					}
					else
						log.info("Server is unavailable");
				}

			}
		}

	}
	public static BufferedReader getInReadConsole() {
		return inReadConsole;
	}

	public static void cleanUp(int ret) {
		//Cleanup for shutdown
		if (serverReceiver != null) {
			serverReceiver.cleanUp();
		}
		try {
			if (serverOut != null) {
				serverOut.close();
			}
			if (serverSocket != null) {
				serverSocket.close();
			}
		} catch (IOException e) {
			/* ignore */
		}
		System.exit(ret);
	}

	//Print auctionlist (split String)
	public static void printAuctionList(String string) {
		//System.out.println("Available auctions:");
		synchronized (auctionList) {
			auctionList = string.split(";");

			for (int a = 0; a < auctionList.length; a++) {
				if(!auctionList[a].equals(""))
					System.out.println(auctionList[a] + " ");

			}
		}
	}

	//___________Stage 2____________________________________________________

	//Try to read keyfile of logged in user
	public static boolean readKeyfile(String username){

		byte[] keyBytes = new byte[1024];
		String pathToSecretKey = clientsKeyDir+username+".key";
		FileInputStream fis;
		try {
			fis = new FileInputStream(pathToSecretKey);
			fis.read(keyBytes);
			fis.close();
		} catch (FileNotFoundException e1) {
			System.out.println("No secretkey found for user "+username);
			return false;
		}
		catch (IOException e) {
			System.out.println("Error: Reading file");
			return false;
		}
		byte[] input = Hex.decode(keyBytes);

		setKey(new SecretKeySpec(input,"HmacSHA256")); 
		return true;
	}

	//___________Stage 1____________________________________________________

	//read privateKey of current user
	private static boolean readRSAPrivateKey(){
		boolean ok = true;
		privateKey = null;
		String pathToPrivateKey = clientsKeyDir+username+".pem";
		log.debug("Path to PrivateKey: "+pathToPrivateKey);
		PEMReader in = null;
		try {
			in = new PEMReader(new FileReader(pathToPrivateKey), new PasswordFinder() {
				@Override
				public char[] getPassword() {
					// reads the password from standard input for decrypting the private key
					System.out.println("Enter pass phrase:");
					try {
						return Client.getInReadConsole().readLine().toCharArray();
					} catch (IOException e) {
						System.out.println("Error: Reading pass phrase");
					}
					return null;
				}
			});
		} catch (FileNotFoundException e1) {
			System.out.println("No PrivateKey File found for user "+username);
			ok = false;
		}
		KeyPair keyPair;
		try {
			keyPair = (KeyPair) in.readObject();
			privateKey = keyPair.getPrivate();
		} catch (IOException e) {
			System.out.println("Error: Reading PrivateKey File for user "+username);
			ok = false;
		} 
		try {
			in.close();
		} catch (IOException ex) {
			/*ignore*/
		}
		//Set privatekey in channel
		decoratedChannel.setPrivateKey(privateKey);
		return ok;
	}

	//read publickey of the server
	private static boolean readRSAPublicKey(){
		boolean ok = true;
		publicKey = null;
		String pathToPublicKey = serverKeyPub;
		log.debug("Path to PublicKey: "+pathToPublicKey);
		PEMReader in = null;
		try {
			in = new PEMReader(new FileReader(pathToPublicKey));
		} catch (FileNotFoundException e) {
			System.out.println("No PublicKey File found for user "+username);
			ok = false;

		} 
		try {
			publicKey = (PublicKey) in.readObject();
		} catch (IOException e) {
			System.out.println("Error: Reading PublicKey File for user "+username);
			ok = false;
		}

		try {
			in.close();
		} catch (IOException e) {
			/*ignore*/
		}

		//set publickey in channel
		decoratedChannel.setPublicKey(publicKey);

		return ok;
	}

	private static String generateClientChallenge() throws UnsupportedEncodingException{
		// generates a 32 byte secure random number 
		SecureRandom secureRandom = new SecureRandom(); 
		final byte[] number = new byte[32]; 
		secureRandom.nextBytes(number);

		// encode into Base64 format 
		byte[] base64Number = Base64.encode(number);
		return new String (base64Number, "utf-8");

	}
	//Create AES Channel
	public static void establishSecureChannel(){
		try {
			decoratedChannel = new AESChannel(new Base64Channel(new TCPChannel(serverSocket)));
			decoratedChannel.setSecretKey(secretKey);
			decoratedChannel.setIvParameter(ivParameter);

			decoratedChannel.send(Client.getServerChallenge().getBytes());

		} catch (NoSuchAlgorithmException e) {
			System.err.println("Error while initialization of AES Secure Channel: " + e.getMessage());
			cleanUp(1);
		} catch (NoSuchPaddingException e) {
			System.err.println("Error while initialization of AES Secure Channel: " + e.getMessage());
			cleanUp(1);
		} catch (IOException e) {
			System.err.println("Error while initialization of AES Secure Channel: " + e.getMessage());
			cleanUp(1);
		}


	}

	public static void sendSignedBids(){
		//send signedBids to Server
		if(Client.getSignedBids()!=null){
			//send signedBids
			for(int i = 0; i<Client.getSignedBids().size(); i++){
				decoratedChannel.send(Client.getSignedBids().get(i).getBytes());
			}
			//refresh
			Client.setSignedBids(null);

		}
	}

	public static SecretKey getSecretKey() {
		return secretKey;
	}
	public static void setSecretKey(String secretKeyBase64) {
		byte[] encodedKey     = Base64.decode(secretKeyBase64);
		SecretKey originalKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
		Client.secretKey = originalKey;
	}
	public static IvParameterSpec getIvParameter() {
		return ivParameter;
	}
	public static void setIvParameter(String ivParameterBase64) {
		Client.ivParameter = new IvParameterSpec(Base64.decode(ivParameterBase64)); 
	}


	//___________Stage 4____________________________________________________

	//Set list of all logged in clients, set after new client logged in
	public static void setClientList(String string) {
		synchronized (clientList) {
			clientList = string.split(";");

		}
	}
	//Print list of all active, logged in clients
	public static void printClientList() {

		synchronized (clientList) {

			for (int a = 0; a < clientList.length; a++) {
				if(!clientList[a].equals(""))
					System.out.println(clientList[a] + " ");

			}
		}
	}

	//Request the timestamp of two other clients
	public static String obtainTimeStampsFromTwoClients(String auctionId, String price){
		String signedBid="";

		int c1 = 0,c2=0;
		if((clientList.length-1) > 2){
			Random rand = new Random();

			c1 = 1 + rand.nextInt(clientList.length-1);
			while(clientList[c1].contains(username)) //can not be the current client user
				c1 = 1 + rand.nextInt(clientList.length-1);

			c2 = 1 + rand.nextInt(clientList.length-1);
			while(c1 == c2 || clientList[c2].contains(username))//can not be the same index or the current client user
				c2 = 1 + rand.nextInt(clientList.length-1);


			//client1:
			String[] temp1 = clientList[c1].split(":");
			client1Host = temp1[0];
			temp1 = temp1[1].split("-");
			client1Port = Integer.parseInt(temp1[0].trim());
			client1Username = temp1[1].trim();
			log.debug("Client1: Host: "+client1Host+" Port: "+client1Port+" Username: "+client1Username);

			//client2:
			String[] temp2 = clientList[c2].split(":");
			client2Host = temp2[0];
			temp2 = temp2[1].split("-");
			client2Port = Integer.parseInt(temp2[0].trim());
			client2Username = temp2[1].trim();
			log.debug("Client2: Host: "+client2Host+" Port: "+client2Port+" Username: "+client2Username);


			try {
				client1Socket = new Socket(client1Host, client1Port);

				client2Socket = new Socket(client2Host, client2Port);

				log.debug("Sockets to Client1 and Client2 established");

				//request and receive TimeStamp from Client 1
				PrintWriter out1;
				BufferedReader in1;

				out1 = new PrintWriter(client1Socket.getOutputStream(), true);
				in1 = new BufferedReader(new InputStreamReader(client1Socket
						.getInputStream()));

				out1.println("!getTimestamp "+auctionId+" "+price);
				String inputString1 = in1.readLine();			
				log.debug("back from client1: "+client1Username+" "+inputString1);

				//request and receive TimeStamp from Client 2

				PrintWriter out2;
				BufferedReader in2;

				out2 = new PrintWriter(client2Socket.getOutputStream(), true);
				in2 = new BufferedReader(new InputStreamReader(client2Socket
						.getInputStream()));

				out2.println("!getTimestamp "+auctionId+" "+price);
				String inputString2 = in2.readLine();			
				log.debug("back from client2: "+client2Username+" "+inputString2);

				//client 1:
				String[] data1 = inputString1.split(" ");
				String string1 = data1[0]+" "+data1[1]+" "+data1[2]+" "+data1[3];
				String timestamp1= data1[3];
				String sign1 = data1[4];
				log.debug("data to verify for client1: "+string1);

				//client 2:

				String[] data2 = inputString2.split(" ");
				String string2 = data2[0]+" "+data2[1]+" "+data2[2]+" "+data2[3];
				String timestamp2= data2[3];
				String sign2 = data2[4];
				log.debug("data to verify for client2: "+string2);

				//make signed bid:
				//format !signedBid <auctionId> <price> Bob:<timestamp1>:<signature1> Carl:<timestamp2>:<signature2>
				signedBid = "!signedBid "+auctionId+" "+price+" "+client1Username+":"+timestamp1+":"+sign1+" "+client2Username+":"+timestamp2+":"+sign2;


			} catch (UnknownHostException e) {
				log.debug("Error TimeStampSocket: "+e.getMessage());
			} catch (IOException e) {
				log.debug("Error TimeStampSocket: "+e.getMessage());
			}


		}
		else
			log.info("!bid command ist not possible, because server is unavailable and there are not enough Clients online to obtain timestamps (at least 2)");

		return signedBid;
	}

	//Test verify timestamp with signature
	public static boolean verifySignature(String dataToVerify, String base64encodedSignature, String signingUser) throws SignatureException, InvalidKeyException, NoSuchAlgorithmException{
		// verify
		Signature instance = Signature.getInstance("SHA512withRSA");
		instance.initVerify(readPublicKeyOfSigningUser(signingUser));
		instance.update(dataToVerify.getBytes()); // der String, der signiert wurde

		byte[] signature = Base64.decode(base64encodedSignature.getBytes());//decode Base64
		return instance.verify(signature); // sig = das byte[] Resultat von oben
	}

	//read publickey of the user who signed the timestamp
	private static PublicKey readPublicKeyOfSigningUser(String signingUser){
		PublicKey publicKey = null;
		String pathToPublicKey = clientsKeyDir+signingUser+".pub.pem";
		log.debug("Path to PublicKey: "+pathToPublicKey);
		PEMReader in = null;
		try {
			in = new PEMReader(new FileReader(pathToPublicKey));
		} catch (FileNotFoundException e) {
			System.out.println("No PublicKey File found for user "+username);


		} 
		try {
			publicKey = (PublicKey) in.readObject();
		} catch (IOException e) {
			System.out.println("Error: Reading PublicKey File for user "+username);

		}

		try {
			in.close();
		} catch (IOException e) {
			/*ignore*/
		}
		return publicKey;
	}

	//close socket if Server is not available anymore
	public static void closeServersocket()
	{
		try {
			Client.serverSocket.close();
		} catch (IOException e) {
			log.debug(e.getMessage());
		}




		//start regular check serversocket thread

		ClientReconnectThread clientReconnectThread = new ClientReconnectThread(serverHost,serverPort);
		clientReconnectThread.start();



	}
	//Reconnect after available server was detected by reconnectthread
	public static void reconnect() {
		try {
			decoratedChannel = new RSAChannel(new Base64Channel(new TCPChannel(serverSocket)));
			decoratedChannel.setPrivateKey(privateKey);
			decoratedChannel.setPublicKey(publicKey);
		} catch (NoSuchAlgorithmException e) {
			log.debug(e.getMessage());
		} catch (NoSuchPaddingException e) {
			log.debug(e.getMessage());
		} catch (IOException e) {
			log.debug(e.getMessage());
		}

		//start receiverthread
		try {
			(serverReceiver = new ReceiverThread(serverSocket)).start();
		} catch (IOException e) {
			log.debug(e.getMessage());
		}
		Client.setRefresh(true);

		//login automatically after reconnect
		decoratedChannel.send(("!login "+username+" "+clientPort+" "+clientChallenge).getBytes());



	}

	//______________________________________________________________________



	//Getters and Setters

	private static void setKey(SecretKeySpec secretKeySpec) {
		key=secretKeySpec;
	}
	public static Key getKey() {
		return key;
	}
	public static Channel getDeoratedChannel() {
		return decoratedChannel;
	}
	public static void setDeoratedChannel(Channel deoratedChannel) {
		Client.decoratedChannel = deoratedChannel;
	}
	public static String getUsername() {
		return username;
	}
	public static void setUsername(String username) {
		Client.username = username;
	}
	public static String getClientsKeyDir() {
		return clientsKeyDir;
	}
	public static String getServerKeyPub() {
		return serverKeyPub;
	}
	public static void setServerKeyPub(String serverKeyPub) {
		Client.serverKeyPub = serverKeyPub;
	}
	public static String getServerChallenge() {
		return serverChallenge;
	}
	public static void setServerChallenge(String serverChallenge) {
		Client.serverChallenge = serverChallenge;
	}
	public static String getClientChallenge() {
		return clientChallenge;
	}
	public static void setClientChallenge(String clientChallenge) {
		Client.clientChallenge = clientChallenge;
	}
	public static PrivateKey getPrivateKey() {
		return privateKey;
	}
	public static void setPrivateKey(PrivateKey privateKey) {
		Client.privateKey = privateKey;
	}
	public static Socket getServerSocket() {
		return serverSocket;
	}
	public static void setServerSocket(Socket serverSocket) {
		Client.serverSocket = serverSocket;
	}
	public static boolean isEnded() {
		return ended;
	}
	public static void setEnded(boolean isEnded) {
		Client.ended = isEnded;
	}
	public static boolean isLoggedIn() {
		return loggedIn;
	}
	public static void setLoggedIn(boolean loggedIn) {
		Client.loggedIn = loggedIn;
	}
	public static ArrayList<String> getSignedBids() {
		return signedBids;
	}
	public static void setSignedBids(ArrayList<String> signedBids) {
		Client.signedBids = signedBids;
	}
	public static boolean isRefresh() {
		return refresh;
	}
	public static void setRefresh(boolean refresh) {
		Client.refresh = refresh;
	}




}
